package org.benetech.util;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import org.benetech.beans.Filter;
import org.benetech.collections.ListMap;
import org.benetech.collections.Tree.ListDirection;
import org.benetech.ref.XML.Regex;
import org.jdom.Attribute;
import org.jdom.Content;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.Text;
import org.jdom.transform.JDOMResult;
import org.jdom.transform.JDOMSource;

/**
 * Utility methods related to modifying XML documents.
 *
 * @author Reuben Firmin
 */
public final class XMLChangeUtils {

	/**
	 * Non constructor.
	 */
	private XMLChangeUtils() {
	}

	/**
	 * Transforms a document by applying the XSL rules contained in the second
	 * document. Uses Saxon.
	 *
	 * @param source The source document to transform
	 * @param transformXSL The XSL document
	 * @param params The parameters to pass to the stylesheet; empty for none
	 * @return The result from the transformation, never null
	 * @throws TransformerException If the transform failed
	 */

	public static Document transform(final Document source, final File transformXSL, final Map<String, String> params)
		throws TransformerException
	{
		// use saxon
		System.setProperty("javax.xml.transform.TransformerFactory", "net.sf.saxon.TransformerFactoryImpl");
		final Transformer transformer = TransformerFactory.newInstance().newTransformer(new StreamSource(transformXSL));
		for (Map.Entry<String, String> param : params.entrySet()) {
			transformer.setParameter(param.getKey(), param.getValue());
		}

		final JDOMResult result = new JDOMResult();
		transformer.transform(new JDOMSource(source), result);
		return result.getDocument();
	}

	/**
	 * Merge a group of documents together on the given element name. The first
	 * document will be the "container"; i.e. the other document's elements will
	 * be inserted at the end of the first's. In other words, assume that each
	 * document has a <body>...</body> element. The content of <body>...</body>
	 * for the second, third, fourth... documents will be inserted sequentially
	 * into the end of the first document's body element. Content outside of the
	 * <body></body> elements in those documents will be dropped. If the
	 * element can't be found in any of the documents, a ParseException will be
	 * thrown.
	 *
	 * @param documents The documents to merge
	 * @param elementName The element name to merge on
	 * @param span True if spans should surround the inserted document contents
	 * @return null if no documents are passed in, the result document otherwise
	 * @throws ParseException if the named element cannot be located. Note that
	 *             if this exception is thrown, the containing document may be
	 *             partially changed, and should be discarded during recovery.
	 */
	public static Document merge(final List<Document> documents, final String elementName, final boolean span)
		throws ParseException
	{
		if (documents.size() > 0) {
			final Document containerDoc = documents.get(0);
			final ListMap<String, Element> containerElements = XMLParseUtils.getDescendentsAsMap(containerDoc
					.getRootElement());
			final Element insertionElement = containerElements.getFirst(elementName);
			if (insertionElement != null) {
				for (int i = 1; i < documents.size(); i++) {
					final Element elementToInsertParent = XMLParseUtils.getDescendentsAsMap(
							documents.get(i).getRootElement()).getFirst(elementName);
					if (elementToInsertParent != null) {
						final Element[] elementToInsertChildren = (Element[]) elementToInsertParent.getChildren()
								.toArray(new Element[0]);
						if (span) {
							final Element containingSpan = new Element("span");
							containingSpan.getAttributes().add(new Attribute("id", "merge-" + i));
							insertionElement.addContent(containingSpan);
							for (Element childToInsert : elementToInsertChildren) {
								childToInsert.detach();
								containingSpan.addContent(childToInsert);
							}
						} else {
							for (Element childToInsert : elementToInsertChildren) {
								childToInsert.detach();
								insertionElement.addContent(childToInsert);
							}
						}
					} else {
						throw new ParseException("Element: " + elementName + " not found in document: "
								+ documents.get(i).getBaseURI(), 0);
					}
				}
			} else {
				throw new ParseException("Element: " + elementName + " not found in document: "
						+ containerDoc.getBaseURI(), 0);
			}
			return containerDoc;
		}
		return null;
	}

	/**
	 * Convert all element names to lower case.
	 *
	 * @param document The document to convert
	 */
	public static void elementNamesToLowerCase(final Document document) {
		final Iterator iterator = document.getRootElement().getDescendants();
		while (iterator.hasNext()) {
			final Object element = iterator.next();
			if (element instanceof Element) {
				((Element) element).setName(((Element) element).getName().toLowerCase());
			}
		}
	}

	/**
	 * Collapses redundant structures. E.g. <span><span><span>id</span></span></span>
	 * will collapse to <span>id</span>, retaining only the attributes of the
	 * top level element. Recursively scans all children of this element. If the
	 * element is a member of the given pattern, and has no children, it is also
	 * removed, regardless of whether it has non-id attributes. Highly
	 * recursive.
	 *
	 * @param element The element to parse.
	 * @param childlessPattern Null to never remove childless elements, unless
	 *            they are otherwise redundant.
	 */
	public static void collapseContentlessElements(final Element element, final Pattern childlessPattern) {
		collapseContentlessElements(element, childlessPattern, new ArrayList<Element>());
	}

	/**
	 * Helper method for {@link #collapseContentlessElements(Element, Pattern)}.
	 *
	 * @param element The element to consider for merging
	 * @param childlessPattern The pattern that identifies childless elements to
	 *            remove; may be null
	 * @param path The path built so far, recursively
	 */
	private static void collapseContentlessElements(final Element element, final Pattern childlessPattern,
			final List<Element> path)
	{
		final Element[] children = (Element[]) element.getChildren().toArray(new Element[0]);
		// no attributes, or only an id
		final boolean noRealAttributes = element.getAttributes().size() == 0
				|| (element.getAttributes().size() == 1 && element.getAttribute("id") != null);
		// if it's textless, childless, and matches the childless pattern,
		// delete it
		if (children.length == 0 && element.getTextNormalize().trim().length() == 0 && childlessPattern != null
				&& childlessPattern.matcher(element.getName()).matches())
		{
			if (path.size() == 0) {
				element.detach();
			} else {
				// delete the entire path
				for (int i = path.size() - 1; i >= 0; i--) {
					path.get(i).detach();
				}
			}
			// if the element name has changed, and we're in a path
		} else if (path.size() > 0 && (!element.getName().equals(path.get(0).getName()) || !noRealAttributes)) {
			mergePath(path);
			// check out the children of this element
			collapseContentlessElements(element, childlessPattern);
			// if we're in a path & the element name is the same, or we need to
			// start a path
		} else if (path.size() > 0 || children.length == 1) {
			// if there's one child (because otherwise merge would unbalance the
			// tree), and we're either starting
			// a new path, or we're in a path and the element has the same name
			// as other elements in the path, and
			// no attributes
			if (children.length == 1
					&& (path.size() == 0 || (element.getName().equals(path.get(0).getName()) && noRealAttributes)))
			{
				path.add(element);
				collapseContentlessElements(children[0], childlessPattern, path);
				// otherwise, there's multiple children, or the name changed, or
				// there were attributes; i.e. we've built
				// as much of the path as we can safely merge.
			} else if (path.size() > 0) {
				// the element's attributes and children will be merged, unless
				// it has a different name, so it is always
				// safe to add it to the path.
				path.add(element);
				if (mergePath(path)) {
					// see if the first element can be deleted now
					collapseContentlessElements(path.get(0), childlessPattern);
				}
			}
			// else, one of the preconditions can't be met, so check out each
			// child to see if they can be collapsed
		} else {
			for (Element child : children) {
				collapseContentlessElements(child, childlessPattern);
			}
		}
	}

	/**
	 * Protected path merge. Merges all elements in the path, sequentially, with
	 * the head of the path, until the element name changes or the root of the
	 * path is reached.
	 *
	 * @param path The path of elements to merge
	 * @return True if merge took place
	 */
	private static boolean mergePath(final List<Element> path) {
		if (path.size() > 1) {
			for (int i = 1; i < path.size(); i++) {
				if (path.get(i).getName().equals(path.get(0).getName())) {
					merge(path.get(0), path.get(i), null);
				} else {
					break;
				}
			}
		} else {
			return false;
		}
		return true;
	}

	/**
	 * Grow the specified Element up the document heirarchy. Works in two modes:
	 * 1) If no terminators are supplied (i.e. is null): grows a child element
	 * to the top of the parent's list of children, swallowing all other
	 * children in the process. In other words, given A[B, C, D, E], and calling
	 * this method with D, modify the hierarchy to A[D[B, C], E] 2) If
	 * terminators are supplied, grow upwards until one of the terminators is
	 * reached, or the root element is reached. E.g., Document = A[B, C, D, E];
	 * method is called with D, B; result is: A[B, D[C], E]. E.g. Document is
	 * A[B[C, D, E, F]; method is called with D, A; result is A[D[B[C, E, F]].
	 *
	 * @param child Element to be grown.
	 * @param terminators Element name(s) to act as a boundary; may be null
	 */
	// XXX this needs to be refactored to reduce complexity
	public static void growToTop(final Element child, final Pattern... terminators) {

		// Only act if the child has a parent
		if (child.getParentElement() != null) {

			// Detach and store references to all children of the child
			final Element[] grandChildren = (Element[]) child.getChildren().toArray(new Element[0]);
			for (Element grandchild : grandChildren) {
				grandchild.detach();
			}

			// If terminators were given, grow the child until one is reached.
			// Stop at the
			// first match closest to the original location of the child.
			if (terminators != null && terminators.length > 0) {

				// check if any of the siblings that come before this element
				// are a terminator
				final Element[] children = (Element[]) child.getParentElement().getChildren().toArray(new Element[0]);
				int lastTerminatorPos = -1;
				int childPos = -1;

				for (int i = 0; i < children.length; i++) {
					final Element sibling = children[i];
					if (sibling.equals(child)) {
						childPos = i;
						break;
					}

					for (Pattern pattern : terminators) {
						final Matcher matcher = pattern.matcher(sibling.getName());
						if (matcher.matches()) {
							lastTerminatorPos = i;
						}
					}
				}

				boolean parentIsTerminator = false;

				// see if the parent matched, if none of the siblings did
				if (lastTerminatorPos < 0) {
					for (Pattern pattern : terminators) {
						final Matcher matcher = pattern.matcher(child.getParentElement().getName());
						if (matcher.matches()) {
							parentIsTerminator = true;
						}
					}
				}

				// we found a terminator in the siblings, or the parent is the
				// terminator, so copy siblings over
				if (lastTerminatorPos >= 0 || parentIsTerminator) {
					if (childPos < 0) {
						throw new RuntimeException("Child was not found!");
					}

					// move siblings between the terminator and the child under
					// the child
					for (int i = lastTerminatorPos + 1; i < childPos; i++) {
						final Element sibling = children[i];
						sibling.detach();
						child.addContent(sibling);
					}
					// there is no terminator in the siblings, so we start
					// swallowing parents in the tree
				} else {
					boolean growUp = true;
					Element next = child.getParentElement();
					child.detach();
					while (growUp) {
						for (Pattern pattern : terminators) {
							final Matcher matcher = pattern.matcher(next.getName());
							if (matcher.matches()) {
								growUp = false;
								break;
							}
						}

						if (growUp) {
							// If the root is reached while we're still trying
							// to grow, we must
							// reattach the element as the topmost child of the
							// root.
							if (next.getParentElement() == null) {
								next.addContent(0, child); // Reattach child
								XMLChangeUtils.swallowSiblings(child);
								growUp = false;
							} else {
								next = next.getParentElement();
							}
						} else {
							next.addContent(0, child); // Reattach child
							XMLChangeUtils.swallowSiblings(child);
						}
					}

					if (child.getParentElement() == null) {
						throw new RuntimeException("Didn't reattach child!");
					}
				}
			} else {
				XMLChangeUtils.growToParent(child);
			}
			// reattach the former children of the child
			for (Element grandchild : grandChildren) {
				child.addContent(grandchild);
			}
		}
	}

	/**
	 * Grow the specified Element up the document heirarchy. Works in two modes:
	 * 1) If no terminators are supplied (i.e. is null): grows a child element
	 * to the top of the parent's list of children, swallowing all other
	 * children in the process. In other words, given A[B, C, D, E], and calling
	 * this method with D, modify the hierarchy to A[D[B, C], E] 2) If
	 * terminators are supplied, grow upwards until one of the terminators is
	 * reached, or the root element is reached. E.g., Document = A[B, C, D, E];
	 * method is called with D, B; result is: A[B, D[C], E]. E.g. Document is
	 * A[B[C, D, E, F]; method is called with D, A; result is A[D[B[C, E, F]].
	 *
	 * @param child Element to be grown.
	 * @param terminators Element name(s) to act as a boundary; may be null
	 */
	public static void growToTop2(final Element child, final Pattern... terminators) {

		final Element parent = child.getParentElement();

		// Only act if the child has a parent
		if (parent != null) {

			if ((terminators == null) || (terminators.length == 0)) {
				XMLChangeUtils.growToAncestor(child, parent);
			} else {
				// If terminator(s) were supplied, then first check the older
				// siblings
				// for a match. If none is found, then check ancestors (parent,
				// grandparent,
				// etc). If the document's root element is reached with no
				// match, then grow
				// all the way to the root.

				final Element sibling = XMLParseUtils.findSiblingBeforeChild(child, terminators);

				if (sibling != null) {
					XMLChangeUtils.growToSibling(child, sibling);

				} else {
					final Element ancestor = XMLParseUtils.findAncestor(child, terminators);
					XMLChangeUtils.growToAncestor(child, ancestor);
				}
			}
		}
	}

	/**
	 * Grow the child Element up the document tree, halting at the sibling.
	 *
	 * @param child The Element to be grown up the document tree.
	 * @param sibling The sibling Element that will halt the growth.
	 */
	public static void growToSibling(final Element child, final Element sibling) {
		final Element[] children = (Element[]) child.getParentElement().getChildren().toArray(new Element[0]);
		boolean siblingFound = false;
		boolean childFound = false;

		for (int i = 0; i < children.length; i++) {
			final Element next = children[i];

			// Only elements that lie between the sibling and the child should
			// be moved.
			if (next.equals(sibling)) {
				siblingFound = true;
			} else if (next.equals(child)) {
				childFound = true;
				break;
			} else {
				if (siblingFound) {
					next.detach();
					child.addContent(next);
				}
			}
		}

		// If either the sibling or the child were not found, then a runtime
		// error must have occurred. These inputs should have been guaranteed
		// prior to the call of the growToSibling function.
		if (!siblingFound) {
			throw new RuntimeException("Sibling Element not found!");
		} else if (!childFound) {
			throw new RuntimeException("Child Element not found!");
		}
	}

	/**
	 * Grow the specified Element up the document tree, halting at the ancestor.
	 *
	 * @param child The Element to be grown up the document tree.
	 * @param ancestor The Element intended to be the new parent of the child.
	 */
	public static void growToAncestor(final Element child, final Element ancestor) {
		Element parent = child.getParentElement();

		if (parent.equals(ancestor)) {
			XMLChangeUtils.growToParent(child);

		} else {
			// If parent is null, we have reached the Document root
			while (parent != null) {
				child.detach();
				// Append to the end of list so swallowSiblings will be able
				// to process all of the new siblings.
				parent.addContent(child);
				// Halt the loop when we reach the ancestor Element
				if (parent.equals(ancestor)) {
					parent = null;
				} else {
					parent = parent.getParentElement();
				}
			}
			// Move all the new siblings underneath the child
			XMLChangeUtils.swallowSiblings(child);
		}
	}

	/**
	 * Move all children of the parent under the child.
	 *
	 * @param child The element that all siblings will be moved under.
	 */
	public static void swallowSiblings(final Element child) {
		final Element parent = child.getParentElement();

		if (parent != null) {
			final Element[] siblings = (Element[]) parent.getChildren().toArray(new Element[0]);
			for (Element sibling : siblings) {
				if (!sibling.equals(child)) {
					sibling.detach();
					child.addContent(sibling);
				}
			}
		}
	}

	/**
	 * Grow a child element to the top of the parent's list of children,
	 * swallowing siblings in the process. For example, if growToParent(D) is
	 * called on Document = A [B, C, D, E], the result would be Document = A [D
	 * [B C] E].
	 *
	 * @param child The element to grow
	 */
	public static void growToParent(final Element child) {
		final Element parent = child.getParentElement();

		if (parent != null) {
			// move all children before the child under the child
			final Element[] siblings = (Element[]) parent.getChildren().toArray(new Element[0]);
			for (Element sibling : siblings) {
				if (sibling.equals(child)) {
					break;
				}
				sibling.detach();
				child.addContent(sibling);
			}
		}
	}

	/**
	 * If terminators are not supplied, grows a child element to the bottom of
	 * the parent's list of children, swallowing all other children in the
	 * process. In other words, given A[B, C, D, E], and calling this method
	 * with D, modify the hierarchy to A[B, C, D[E]] TODO - does this need the
	 * same terminator logic as growToTop?
	 *
	 * @param child Child of the parent
	 */
	public static void growToBottom(final Element child) {
		if (child.getParentElement() != null) {
			// move all children after the child under the child
			final Element[] children = (Element[]) child.getParentElement().getChildren().toArray(new Element[0]);
			boolean after = false;
			for (Element sibling : children) {
				if (sibling.equals(child)) {
					after = true;
				} else if (after) {
					sibling.detach();
					child.addContent(sibling);
				}
			}
		}
	}

	/**
	 * Inserts the given element after the specified new sibling. E.g., given
	 * the document A[B, C[D, E[F]]], and called with F, C, result is A[B, C[D,
	 * E], F].
	 *
	 * @param element The element to insert. It can already exist in the tree,
	 *            or be new.
	 * @param newSibling The new sibling to insert after
	 */
	public static void insertAfter(final Element element, final Element newSibling) {
		element.detach();
		final List siblings = newSibling.getParentElement().getContent();
		int i = 0;
		for (; i < siblings.size(); i++) {
			if (siblings.get(i).equals(newSibling)) {
				break;
			}
		}
		newSibling.getParentElement().addContent(i + 1, element);
	}

	/**
	 * Inserts the given element after the specified new sibling. E.g., given
	 * the document A[B, C[D, E[F]]], and called with F, C, result is A[B, F,
	 * C[D, E]].
	 *
	 * @param element The element to insert. It can already exist in the tree,
	 *            or be new.
	 * @param newSibling The new sibling to insert after
	 */
	public static void insertBefore(final Element element, final Element newSibling) {
		element.detach();
		final List siblings = newSibling.getParentElement().getContent();
		int i = 0;
		for (; i < siblings.size(); i++) {
			if (siblings.get(i).equals(newSibling)) {
				break;
			}
		}
		newSibling.getParentElement().addContent(i, element);
	}

	/**
	 * Parse all children of this element. If they qualify, and two or more
	 * adjacent elements pass the comparison filter, then merge them. Adjacency
	 * is determined either depth or breadth first. If breadth first, then they
	 * must share the same parent to be considered adjacent.
	 *
	 * @param root the root element to start from
	 * @param comparisonFilter determines whether the two elements can be merged
	 *            (e.g. they might have the same name)
	 * @param qualifyingFilter determines if the element being considered
	 *            qualifies for merging (e.g. it's a heading)
	 * @param direction The direction of adjacency
	 */
	public static void mergeAdjacentElements(final Element root, final Filter<Element, ?> comparisonFilter,
			final Filter<Element, ?> qualifyingFilter, final ListDirection direction)
	{
		final List<Element> list = XMLParseUtils.flattenToList(root);
		int elementIndex = 0;
		for (int nextIndex = 1; nextIndex < list.size(); nextIndex++) {
			final Element element = list.get(elementIndex);
			if (qualifyingFilter.accept(element)) {
				final Element next = list.get(nextIndex);
				// the next element is a match, merge them. if we're in breadth
				// first
				// mode, make sure the elements share the same parent; otherwise
				// we don't count them as being
				// adjacent
				if (qualifyingFilter.accept(next)
						&& (direction.equals(ListDirection.DEPTH) || (direction.equals(ListDirection.BREADTH) && XMLParseUtils
								.adjacent(element, next))) && comparisonFilter.accept(element, next))
				{
					merge(element, next, null);
				} else {
					// next didn't pass, so bump up the element under comparison
					elementIndex = nextIndex;
				}
			} else {
				// element under comparison didn't pass, so compare next one
				elementIndex = nextIndex;
			}
		}
	}

	/**
	 * Merge the two elements. All children of the secondary element are
	 * detached and attached to the primary. All attributes from the secondary
	 * are applied to the primary, iff they don't collide with the primary's
	 * attributes. Text from the secondary is appended to the primary's text.
	 * Finally, the secondary is detached and orphaned.
	 *
	 * @param primary The element to merge to
	 * @param secondary The element to merge from
	 * @param attributeFilter Determines which attributes are copied; null for
	 *            all that don't collide
	 */
	public static void merge(final Element primary, final Element secondary, final Filter<Attribute, ?> attributeFilter)
	{
		merge(primary, secondary, attributeFilter, true);
	}

	/**
	 * Merge the two elements. All children of the secondary element are
	 * detached and attached to the primary. All attributes from the secondary
	 * are applied to the primary, iff they don't collide with the primary's
	 * attributes. Text from the secondary is appended to the primary's text.
	 * Finally, the secondary is detached and orphaned.
	 *
	 * @param primary The element to merge to
	 * @param secondary The element to merge from
	 * @param attributeFilter Determines which attributes are copied; null for
	 *            all that don't collide
	 * @param addSpaces boolean to set wether or not to add whitespaces between
	 *            elements.
	 */

	public static void merge(final Element primary, final Element secondary,
			final Filter<Attribute, ?> attributeFilter, boolean addSpaces)
	{
		if (secondary.equals(primary.getParentElement())) {
			throw new UnsupportedOperationException("Cannot merge downwards, as that implies merging a "
					+ "child with itself");
		}
		final boolean secondaryHasText = !secondary.getText().equals("");
		final Content[] primaryContent = (Content[]) primary.removeContent().toArray(new Content[0]);
		final Content[] secondaryContent = (Content[]) secondary.getContent().toArray(new Content[0]);
		boolean spaceNeeded = false;

		int primaryIndex = 0;
		// copy until we find the secondary
		for (; primaryIndex < primaryContent.length && !primaryContent[primaryIndex].equals(secondary); primaryIndex++)
		{
			if (primaryContent[primaryIndex] instanceof Text && addSpaces) {
				spaceNeeded = true;
			}
			primary.addContent(primaryContent[primaryIndex]);
		}

		// copy secondary content
		for (Content child : secondaryContent) {
			child.detach();
			if (child instanceof Text && spaceNeeded) {
				primary.addContent(" ");
				spaceNeeded = false;
			}
			primary.addContent(child);
		}
		if (secondaryHasText && addSpaces) {
			spaceNeeded = true;
		}
		// copy rest of primary
		for (primaryIndex += 1; primaryIndex < primaryContent.length; primaryIndex++) {
			if (primaryContent[primaryIndex] instanceof Text && spaceNeeded) {
				primary.addContent(" ");
				spaceNeeded = false;
			}
			primary.addContent(primaryContent[primaryIndex]);
		}
		// copy attributes
		final Map<String, Attribute> primaryAttributes = XMLParseUtils.getAttributeMap(primary);
		final Attribute[] secondaryAttributes = (Attribute[]) secondary.getAttributes().toArray(new Attribute[0]);
		for (Attribute attribute : secondaryAttributes) {
			attribute.detach();
			// if it doesn't exist, add it
			if (primaryAttributes.get(attribute.getName()) == null
					&& (attributeFilter == null || attributeFilter.accept(attribute)))
			{
				primary.getAttributes().add(attribute);
			}
		}
		secondary.detach();
	}

	/**
	 * Clean up the XML by declaring undeclared entitites so that SAX is able to
	 * parse the file successfully.
	 *
	 * @param file The unparsed file
	 * @throws IOException if file access failss
	 */
	public static void declareUndeclaredEntitites(final File file) throws IOException {
		final String fileContents = FileUtils.getFileAsString(file, null);
		final Matcher entityMatcher = Regex.ENTITY_PATTERN.matcher(fileContents);
		final HashSet<String> entities = new HashSet<String>();
		while (entityMatcher.find()) {
			entities.add(entityMatcher.group(1));
		}
		if (entities.size() > 0) {
			final Matcher docTypeMatcher = Regex.DOCTYPE_PATTERN.matcher(fileContents);
			// build entity string - not perfect, but gets past the parser at
			// least
			final StringBuffer entityString = new StringBuffer(" [");
			for (String entity : entities) {
				entityString.append("<!ENTITY ").append(entity).append(" \"").append(entity).append("\">");
			}
			entityString.append("]");
			StringBuffer sb;
			if (docTypeMatcher.find()) {
				int i = 1;
				int index = docTypeMatcher.end();
				while (i > 0) {
					if (fileContents.length() < index) {
						throw new IOException("Cannot parse DocType; no terminator found. Index " + index);
					}
					final char c = fileContents.charAt(index++);
					if (c == '<') {
						i++;
					} else if (c == '>') {
						i--;
					}
				}
				// before the end of element
				index = index - 1;
				if (fileContents.charAt(index) == '/') {
					index--;
				}

				sb = new StringBuffer(fileContents.substring(0, index)).append(entityString).append(
						fileContents.substring(index));

			} else {
				sb = new StringBuffer(entityString).append(fileContents);
			}
			FileUtils.writeToFile(file, sb.toString().getBytes());
		}
	}

	/**
	 * Force the namespace on this element, and all of its descendents.
	 *
	 * @param root The root element
	 * @param namespace The namespace to force
	 */
	public static void forceNamespace(final Element root, final Namespace namespace) {
		final Iterator descendents = root.getDescendants();
		while (descendents.hasNext()) {
			final Object o = descendents.next();
			if (o instanceof Element) {
				final Element desc = (Element) o;
				desc.setNamespace(namespace);
			}
		}
	}
}
